import random
import requests
import pandas as pd
import datetime
import time
import re
import threading
import requests
from Crypto.Cipher import AES
import base64
from random import randrange
from bs4 import BeautifulSoup
import re
import time
import datetime
STATUS = {'free': '空闲', 'busy': '使用中', 'lock': '锁定', 'occ': '已预约'}


class AESCrypt:
    """
    csu encrypt.js实现过程如下：
    function getAesString(data, key0, iv0) {
        key0 = key0.replace(/(^\s+)|(\s+$)/g, "");
        var key = CryptoJS.enc.Utf8.parse(key0);
        var iv = CryptoJS.enc.Utf8.parse(iv0);
        var encrypted = CryptoJS.AES.encrypt(data, key, {
            iv: iv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7
        });
        return encrypted.toString();
    }
    function encryptAES(data, aesKey) {
        if (!aesKey) {
            return data;
        }
        var encrypted = getAesString(randomString(64) + data, aesKey, randomString(16));
        return encrypted;
    }
    var $aes_chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
    var aes_chars_len = $aes_chars.length;
    function randomString(len) {
        var retStr = '';
        for (i = 0; i < len; i++) {
            retStr += $aes_chars.charAt(Math.floor(Math.random() * aes_chars_len));
        }
        return retStr;
    }
    """

    def __init__(self, key, mode, iv, data):
        self.key = key.encode('utf-8')
        self.mode = mode
        self.iv = iv.encode('utf-8')
        self.data = self.pkcs7(data)
        self.cipher = AES.new(self.key, self.mode, self.iv)
        self.encryptedStr = None

    def encrypt(self):
        self.encryptedStr = base64.b64encode(self.cipher.encrypt(self.data))
        return self.encryptedStr

    def pkcs7(self, data, block_num=16):
        """
        填充规则：如果长度不是block_num的倍数，余数使用余数进行补齐
        :return:
        """
        pad = block_num - len(data.encode('utf-8')) % block_num
        data = data + pad * chr(pad)
        return data.encode('utf-8')


class CSULoginHelper:
    def __init__(self, username, password):
        self.username = username
        self.password = password
        self.key = None
        self.aes_crypt = None

        self.base_url = 'http://libzw.csu.edu.cn/cas/index.php?callback=http://libzw.csu.edu.cn/home/web/f_second'

        self.login_url = 'https://ca.csu.edu.cn/authserver/login?service=http%3A%2F%2Flibzw.csu.edu.cn%2Fcas%2Findex.php%3Fcallback%3Dhttp%3A%2F%2Flibzw.csu.edu.cn%2Fhome%2Fweb%2Ff_second'
        self.login_url = "https://ca.csu.edu.cn/authserver/login?callback=http://libzw.csu.edu.cn/home/web/f_second"
        # self.login_url = "https://ca.csu.edu.cn/authserver/login?callback=www.baidu.com"

        # self.save_url = "https://wxxy.csu.edu.cn/ncov/wap/default/save"
        # self.info = None
        self.sess = requests.Session()

    def login(self):
        def __login_passwd_aes(mode=AES.MODE_CBC):
            def __random_str(num):
                chars = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678"
                return ''.join([chars[randrange(len(chars))] for i in range(num)])

            passwd_with_salt, iv = __random_str(
                64) + self.password, __random_str(16)
            self.aes_crypt = AESCrypt(self.key, mode, iv, passwd_with_salt)
            return self.aes_crypt.encrypt()

        # try:
        login_res = self.sess.get(self.base_url, allow_redirects=True)
        # print(login_res.text)
        login_html = login_res.content.decode()
        login_soup = BeautifulSoup(login_html, "html.parser")
        login_form = login_soup.find("form", id="pwdFromId")
        self.key = login_form.find("input", id="pwdEncryptSalt")['value']
        pwd = __login_passwd_aes()
        # print(pwd)
        login_data = {
            "username": self.username,
            "password": pwd,
            "captcha": '',
            "rememberMe": login_form.find("input", id="rememberMe")['value'],
            "_eventId": login_form.find("input", id="_eventId")['value'],
            "cllt": login_form.find("input", id="cllt")['value'],
            "dllt": login_form.find("input", id="dllt")['value'],
            "lt": login_form.find("input", id="lt")['value'],
            "execution": login_form.find("input", id="execution")['value']
        }
        # session中cookies单点登录相关的key改变
        r = self.sess.post(self.login_url, data=login_data,
                           allow_redirects=True)
        # print(r.headers)

        access_token = r.cookies['access_token']
        return access_token
        # except Exception as e:
        # print("中南大学统一登录过程出错")
        # exit(1)


class CSULibraryHelper:
    def __init__(self, username='', password='') -> None:
        self.set_user_passwd(username, password)
        self.token = ''
        if username != "":
            try:
                self.login()
            except:
                print('> 登录失败')
        self.AREA_MAP_TRUE2ID, self.AREA_MAP_ID2TRUE, self.TREE = self.get_map_areaNumber2areaTureID()

    def set_token(self, token):
        self.token = token

    def set_user_passwd(self, username, password):
        self.username = username
        self.password = password

    def get_header(self):
        user_agents = [
            'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36 OPR/26.0.1656.60',
            'Opera/8.0 (Windows NT 5.1; U; en)',
            'Mozilla/5.0 (Windows NT 5.1; U; en; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 Opera 9.50',
            'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera 9.50',
            'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0',
            'Mozilla/5.0 (X11; U; Linux x86_64; zh-CN; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10',
            'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.57.2 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2',
            'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36',
            'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11',
            'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.133 ',
            'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36'
        ]
        headers = {
            "Host": "libzw.csu.edu.cn",
            "Connection": "keep-alive",
            # "Content-Length": "86",
            "Accept": "application/json, text/javascript, */*; q=0.01",
            "X-Requested-With": "XMLHttpRequest",
            "User-Agent": user_agents[random.randint(0, 10)],
            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
            "Origin": "http://libzw.csu.edu.cn",
            "Referer": "www.baidu.com",
            "Accept-Encoding": "gzip, deflate",
            "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
        }
        return headers

    def fix_BenBu_areaName(self, area):
        r1 = re.compile(r'\w区')
        r2 = re.compile(r'.楼')
        area_name = area['nameMerge']
        areaABCD = r1.findall(area_name.split('-')[2])[0][0]
        areaFloor = r2.findall(area_name)[0][0]
        new_area_name = '本部-{:s}楼-{:s}区'.format(areaFloor, areaABCD)
        return new_area_name

    def fix_TieDao_areaName(self, area):
        r1 = re.compile(r'\w区')
        r2 = re.compile(r'.楼')
        area_name = area['nameMerge']
        areaABCD = r1.findall(area_name.split('-')[2])[0][0]
        areaFloor = r2.findall(area_name)[0][0]
        new_area_name = '铁道-{:s}楼-{:s}区'.format(areaFloor, areaABCD)
        return new_area_name

    def fix_XiangYa_areaName(self, area):
        r1 = re.compile(r'\w区')
        r2 = re.compile(r'.楼')
        area_name = area['nameMerge']
        # areaABCD = r1.findall(area_name.split('-')[2])[0][0]
        areaFloor = r2.findall(area_name)[0][0]
        new_area_name = '湘雅新校区-{:s}楼'.format(areaFloor)
        return new_area_name

    def get_map_areaNumber2areaTureID(self, save=False):

        tree = {'X': {}, 'B': {}, 'T': {}, 'Y': {}}
        url = 'http://libzw.csu.edu.cn/api.php/areas/20/tree/1'
        r = requests.get(url=url, headers=self.get_header())
        librarys = pd.DataFrame(r.json()['data']['list'])
        BENBU = 2
        XINXIAO = 0
        TIEDAO = 3
        XIANGYA = 5
        mapping1 = {}
        mapping2 = {}
        f = {
            '新校区': 'X',
            '本部': 'B',
            '铁道': 'T',
            '湘雅新校区': 'Y',
            '一楼': 'F1',
            '二楼': 'F2',
            '三楼': 'F3',
            '四楼': 'F4',
            '五楼': 'F5',
            '六楼': 'F6',
            '七楼': 'F7',
        }
        for school_idx in [XINXIAO, BENBU, TIEDAO, XIANGYA]:
            library = librarys.loc[school_idx]
            # 0新校 1??? 2本部 3铁道 4医学院 5 湘雅新校区
            SCHOOL_XTB = ['X', '', 'B', 'T', '', 'Y'][school_idx]
            floors = pd.DataFrame(library['_child'])
            if SCHOOL_XTB == 'T':
                floor_count = 0
            else:
                floor_count = 1

            for floor in floors['_child']:
                floor_count += 1
                tree[SCHOOL_XTB]['F'+str(floor_count)] = {}
                areas = pd.DataFrame(floor)
                for i in range(areas.shape[0]):
                    area = areas.loc[i]
                    area_id = area['id']
                    area_name = area['nameMerge']
                    if school_idx == BENBU:
                        area_name = self.fix_BenBu_areaName(area)
                    elif school_idx == TIEDAO:
                        area_name = self.fix_TieDao_areaName(area)
                    elif school_idx == XIANGYA:
                        area_name = self.fix_XiangYa_areaName(area)
                    try:
                        if SCHOOL_XTB == 'Y':
                            schoolArea, floor = area_name.split('-')
                            area = 'A'
                        else:
                            schoolArea, floor, area = area_name.split('-')
                    except:
                        print(area_name.split('-'))
                        continue
                    if school_idx == XINXIAO and len(area) != 2:
                        continue
                    schoolArea, floor, area = f[schoolArea], f[floor], area[0]
                    area_ture_id = schoolArea+floor+area
                    mapping1[area_ture_id] = str(area_id)
                    mapping2[str(area_id)] = area_ture_id
                    tree[schoolArea][floor][area] = {
                        'area_info': (str(area_id), area_ture_id)}
        if save:
            data_list = []
            keys = list(mapping1.keys())
            values = list(mapping1.values())
            for i in range(len(mapping1)):
                data = {'true_id': keys[i], 'area_id': values[i]}
                data_list.append(data)
                df = pd.DataFrame(data_list)
                df.to_csv('area_map.csv')
        return mapping1, mapping2, tree

    def get_segment(self, area_id, today):
        r = requests.get(url='http://libzw.csu.edu.cn/api.php/areadays/'+str(area_id),
                         headers=self.get_header()
                         )
        data_dicts = r.json()['data']['list']
        available_segment = []
        for data in data_dicts:
            available_segment.append(data['id'])
        # print(available_segment)
        try:
            if today:
                return str(min(available_segment))
            else:
                return str(max(available_segment))
        except:
            return 0

    def get_token1(self, user, password):
        try:
            # r = requests.get('http://libzw.csu.edu.cn/api.php/login/', headers=get_header(),
            #                  params={
            #     'username': user,
            #     'password': password,
            #     # 'callback':'http://libzw.csu.edu.cn/web/seat3?area=79'
            #     'from': 'mobile'
            # })
            r = requests.get('http://libzw.csu.edu.cn/Api/auto_user_check/', headers=self.get_header(),
                             params={
                'user': user,
                'p': password,
                'callback': 'www.baidu.com'
            })
            # print(r.headers['Set-Cookie'])
            token = r.headers['Set-Cookie'].split('token=')[1][:32]
            # s = r.json()['data']['_hash_']
            # userid = s['userid']
            # access_token = s['access_token']
            return True, token

        except:
            print('登录失败', r.json())
            return False, ''

    def get_token2(self, user, password):
        try:
            # r = requests.get('http://libzw.csu.edu.cn/api.php/login/', headers=get_header(),
            #                  params={
            #     'username': user,
            #     'password': password,
            #     # 'callback':'http://libzw.csu.edu.cn/web/seat3?area=79'
            #     'from': 'mobile'
            # })
            r = requests.get('http://libzw.csu.edu.cn/api.php/login/', headers=self.get_header(),
                             params={
                'username': user,
                'p': password,
                'callback': 'http://libzw.csu.edu.cn/web/seat3?area=79'
            })
            # print(r.json())
            s = r.json()['data']['_hash_']
            userid = s['userid']
            access_token = s['access_token']
            return True, access_token

        except:
            print('登录失败', r.json())
            return False, ''

    def get_token3(self, user, password):
        helper = CSULoginHelper(user, password)
        token = helper.login()
        return token

    def area_TrueID2char(self, s):
        a = {'X': '新校', 'B': '本部', 'T': '铁道', 'Y': '湘雅新校区'}
        b = {
            'F1': '一楼',
            'F2': '二楼',
            'F3': '三楼',
            'F4': '四楼',
            'F5': '五楼',
            'F6': '六楼',
            'F7': '七楼',
        }
        output = a[s[0]]+'-'+b[s[1:3]]+'-'+s[-1]+'区'
        return output

    def get_single_area_info(self, area, today=True):
        area_id = self.AREA_MAP_TRUE2ID[area]
        segment = self.get_segment(area_id, today)
        ts = datetime.datetime.now()
        date = ts.date()
        time = ts.time().strftime('%H:%M')
        response = requests.get(
            headers=self.get_header(),
            url='http://libzw.csu.edu.cn/api.php/spaces_old'        # url='http://libzw.csu.edu.cn//api.php/spaces_old?\
            #     area='+str(area) +\
            #     '&segment='+str(segment)+\
            #     '&day='+date+\
            #     '&startTime='+time+'\
                #     &endTime=22:00',
            , params={
                'area': str(area_id),
                'segment': str(segment),
                'day': date.isoformat() if today else date+datetime.timedelta(days=1),
                'startTime': time if today else "7:30",
                'endTime': '22:00',
            }
        )
        # response.encoding = 'utf-8-sig'
        df_area = pd.DataFrame(response.json()['data']['list'], columns=[
            'id', 'status_name', 'name'])
        df_area = df_area[df_area['status_name'] != STATUS['lock']]
        return {'ch': self.area_TrueID2char(area), 'segment': segment, 'area': area, 'area_id': area_id, 'df': df_area}

    def dfs_TREE(self, tree, function=lambda x: x):
        res = []
        d1 = list(tree.values()) if type(tree) == dict else tree
        for i in range(len(d1)):
            d2 = list(d1[i].values())if type(d1[i]) == dict else d1[i]
            res.append([])
            for j in range(len(d2)):
                d3 = list(d2[j].values())if type(d2[j]) == dict else d2[j]
                res[i].append([])
                for k in range(len(d3)):
                    value = d3[k]
                    # print(value)
                    f_value = function(value)
                    # print(f_value)
                    res[i][j].append(f_value)
        return res

    def get_all_seat_info(self, today=True):
        return self.get_seat_info(self.process_input('X B T Y'), today)

    def get_seat_info(self, areas, today=True):
        res_list = []
        threadList = [MyThread(args=[self, area, today, res_list])
                      for area in areas]
        for t in threadList:
            t.setDaemon(True)
            t.start()
        for i in threadList:
            i.join()
        return res_list

    def regular_search_area(self, s, output_TrueID=True):
        res = []
        s_length = len(s)
        if s_length == 1:  # x
            key_msg = [s[0]]
        elif s_length == 2:  # x5
            key_msg = [s[0], 'F'+s[1]]
        elif s_length == 3:
            if s[1] == 'F':  # xf5
                key_msg = [s[0], 'F'+s[2]]
            else:  # x5a
                key_msg = [s[0], 'F'+s[1], s[2]]
        else:
            key_msg = [s[0], 'F'+s[2], s[3]]
        cur_tree = self.TREE.copy()
        for i in range(len(key_msg)):
            try:
                cur_tree = cur_tree[key_msg[i]]
            except:
                print('> [WARNING] regular_search_area')
                break
        stack = [cur_tree]
        while len(stack) > 0:
            # print(stack)
            p = stack.pop(0)
            for key, value in p.items():
                if key == 'area_info':
                    if output_TrueID:
                        res.append(value[1])
                    else:
                        res.append(value[0])

                else:
                    stack.append(value)
        return res

    def login(self):
        self.token = self.get_token3(self.username, self.password)
        print(self.token)
        return self.token

    def book_seat(self, seat_id, segment):
        response = requests.post(
            headers=self.get_header(),
            url='http://libzw.csu.edu.cn/api.php/spaces/' +
                str(seat_id)+'/book',
            data={
                'userid': self.username,
                'access_token': self.token,
                'segment': str(segment),
                'type': 1
            },
            cookies=''
        )
        return response

    def get_profile(self):
        r = requests.get(url='http://libzw.csu.edu.cn/api.php/profile/books',
                         headers=self.get_header(),
                         params={
                             'userid': self.username,
                             'access_token': self.token
                         })
        return pd.DataFrame(r.json()['data']['list'])

    def delete_your_book(self):
        # 预约开始提醒 预约成功 用户取消 已使用 已关闭
        waiting_list = []
        profile = self.get_profile()
        for i in range(profile.shape[0]):
            info = profile.iloc[i]
            if info['statusName'] == '预约开始提醒':
                waiting_list.append(info['id'])
        for info_id in waiting_list:
            r = requests.post(url='http://libzw.csu.edu.cn/api.php/profile/books',
                              headers=self.get_header(),
                              params={
                                  'id': info_id,
                                  'userid': self.username,
                                  'access_token': self.token,
                                  '_method': 'delete'
                              })
            print('> 服务器反馈:', r.json()['msg'])

    def find_free_seat(self, seat_info):
        free_seatID = {}
        for i in range(len(seat_info)):
            info = seat_info[i]
            df = info['df']
            segment = info['segment']
            df_free = df[df['status_name'] == STATUS['free']]
            area = info['area']
            area_id = info['area_id']
            if df_free.shape[0] == 0:
                pass
                # print('> %s 区座位已满' %
                #   self.area_TrueID2char(info['area_true_id']))
            else:
                free_id = df_free['id'].tolist()
                true_id = df_free['name'].tolist()
                free_seatID[area] = {
                    'area_id': area_id,
                    'segment': segment,
                    'free_seat_num': len(free_id),
                    'free_id': free_id,
                    'true_id': true_id}
                # ======== 输出
                # for i in range(len(free_id)):
                # free_id[i] = self.AREA_MAP_ID2TRUE[free_id[i]]
                # print('> %s 区座位空闲座位号' % self.area_TrueID2char(
                #     info['area_true_id']), true_id)
        if len(free_seatID) == 0:
            # print('> 所选所有区域座位已满')
            return {}
        else:
            # print('> 空闲座位信息:', free_seatID)
            return free_seatID

    def output_all_seat_info(self, all_info_tree):
        def f(x):
            df = x['df']
            segment = x['segment']
            area_ture_id = x['area_true_id']
            ch = x['ch']
            area_id = x['area_id']
            df_free = df[df['status_name'] == STATUS['free']]
            df_busy = df[df['status_name'] == STATUS['busy']]
            df_occ = df[df['status_name'] == STATUS['occ']]
            print('{:s} 空闲座位:{:d} 使用座位:{:d} 已预约:{:d}'.format(
                ch, df_free.shape[0], df_busy.shape[0], df_occ.shape[0]))
        self.dfs_TREE(all_info_tree, lambda x: f(x))

    def process_input(self, s):
        res = []
        for single in s.split(' '):
            reg = self.regular_search_area(single)
            res.extend(reg)
        # print('> 处理字符串结果：', res)
        return res

    def book(self, xh, passwd, area):
        # try:
        seat_info = self.get_seat_info([area], True)
        ids = self.find_free_seat(seat_info)
        idx = random.randint(0, len(ids[area]['free_id'])-1)
        seat_id = ids[area]['free_id'][idx]
        seat_true_id = ids[area]['true_id'][idx]
        segment = ids[area]['segment']
        self.set_user_passwd(xh, passwd)
        self.login()
        r = self.book_seat(seat_id, segment)
        msg = '> 服务器反馈: {:s}'.format(area), r.json()['msg']
        return msg
        # except:
        #     return '预约失败'

    def main(self, s, today=True):
        chosen_areas = self.process_input(s)
        for i in range(len(chosen_areas)):
            print('> 已选区域:', self.area_TrueID2char(chosen_areas[i]))
            # chosen_areas[i] = self.AREA_MAP_TRUE2ID[chosen_areas[i]]
        print()
        success = False
        ret1 = True
        while not success:
            time.sleep(2)
            if ret1:
                seat_info = self.get_seat_info(chosen_areas, today)
                ids = self.find_free_seat(seat_info)
                if len(ids) > 0:
                    idx = random.randint(0, len(ids[0]['free_id'])-1)
                    seat_id = ids[0]['free_id'][idx]
                    seat_true_id = ids[0]['true_id'][idx]
                    segment = ids[0]['segment']
                    print('> 拟预定座位ID:', (seat_true_id, seat_id))
                    r = self.book_seat(seat_id, segment)
                    print('> 服务器反馈:', r.json()['msg'])
                    success = True
                else:
                    print('> 所选所有区域座位已满')
                    success = False

            else:
                print('> 获取token失败 :）')
                break

    def hahaha(self):
        r = requests.get(url='http://libzw.csu.edu.cn/api.php/signout',
                         headers=self.get_header(),
                         params={
                             #  'userid': self.username,
                             #  'access_token': self.token
                         })
        print(r.json())
        # return pd.DataFrame(r.json()['data']['list'])

    def signout_seat(self, segment):
        response = requests.post(
            headers=self.get_header(),
            url='http://libzw.csu.edu.cn/api.php/spaces/10/',
            data={
                'userid': self.username,
                'access_token': self.token,
                'segment': str(segment),
                'type': 1
            },
            cookies=''
        )
        return response


class MyThread(threading.Thread):
    def __init__(self, args) -> None:
        super().__init__()
        self.args = args

    def run(self):
        helper = self.args[0]
        area = self.args[1]
        today = self.args[2]
        res_list = self.args[3]
        res = helper.get_single_area_info(area, today)
        res_list.append(res)


if __name__ == '__main__':
    pass
